In [50]:
#Importing packages
import math
import colorsys
from itertools import combinations

import numpy as np
import matplotlib.pyplot as plt

from mpl_toolkits.mplot3d import Axes3D
from PIL import Image, ImageColor

from sklearn.cluster import KMeans, AgglomerativeClustering
In [2]:
def image_to_numpy(filename, colorspace):
    image = Image.open(filename).convert(colorspace)
    data = np.asarray(image)
    
    y_size = data.shape[0]
    x_size = data.shape[1]
    
    data2d = np.reshape(data,(y_size * x_size, 3)) 
    
    #need to cut from size of notebook XD
    #scaling_param = int((x_size/800 + y_size/600) / 2) + 1
    #height_list = [i for i in range(0, y_size, scaling_param)] 
    #width_list = [i for i in range(0, x_size, scaling_param)]

    #data = data[height_list, :, :]
    #data = data[:, width_list, :]
    #data2d = np.reshape(data,(len(height_list)*len(width_list), 3)) 
    
    return data, data2d
In [3]:
def reduce_image_res(filename, colorspace):
    image = Image.open(filename).convert(colorspace)
    data = np.asarray(image)
    
    y_size = data.shape[0]
    x_size = data.shape[1]
    
    #reduce image to ~200x100 pixel size
    scaling_param = int((x_size/200 + y_size/100) / 2)
    
    height_list = [i for i in range(0, y_size, scaling_param)] 
    width_list = [i for i in range(0, x_size, scaling_param)]
    
    data = data[height_list, :, :] #remove 4th col too if rgba
    data = data[:, width_list, :]
    data2d = np.reshape(data,(len(height_list)*len(width_list), 3))
    
    return data, data2d, scaling_param
In [4]:
def scatterplot_cs(data2d, colorspace, labels=None):
    fig = plt.figure(figsize=(12,9))
    ax = fig.gca(projection='3d')

    x = data2d[:,0]
    y = data2d[:,1]
    z = data2d[:,2]
    
    if labels is not None:
        ax.scatter(x, y, z, c=labels.astype(float), edgecolor="k")
    else:
        ax.scatter(x, y, z, edgecolor="k")        
    
    if colorspace == "RGB":
        ax.set_xlabel('Red Value')
        ax.set_ylabel('Green Value')
        ax.set_zlabel('Blue Value')
    elif colorspace == "HSV":
        ax.set_xlabel('Hue')
        ax.set_ylabel('Saturation')
        ax.set_zlabel('Value')
    elif colorspace == "YCbCr":
        ax.set_xlabel('Y - Luma Comp.')
        ax.set_ylabel('Cb - Blue Diff.')
        ax.set_zlabel('Cr - Red Diff.')

    plt.show()
In [21]:
filename = "london-majus1.jpg"
data, data2d, scaling_param = reduce_image_res(filename, "RGB")
scatterplot_cs(data2d, "RGB", labels=None)
In [22]:
data, data2d, scaling_param = reduce_image_res(filename, "HSV")
scatterplot_cs(data2d, "HSV", labels=None)
In [24]:
data, data2d, scaling_param = reduce_image_res(filename, "YCbCr")
scatterplot_cs(data2d, "YCbCr", labels=None)
In [17]:
def agglomerative(data2d, n):
    """Only to use with small images, 200x100 pixel max!"""
    
    est = AgglomerativeClustering(n_clusters=n, linkage="complete")
    est.fit(data2d)
    labels = est.labels_
    
    #calculate centroids ourselves
    labels_resh = np.array(labels).reshape((len(labels), 1))
    data2d = np.append(data2d, labels_resh, axis=1)
    
    n_points = [0 for i in range(n)]
    y_sum = [0 for i in range(n)]
    x_sum = [0 for i in range(n)]
    z_sum = [0 for i in range(n)]
    
    for point_i in range(len(data2d)):
        point = data2d[point_i]
        
        n_points[point[3]] += 1
        y_sum[point[3]] += point[0]
        x_sum[point[3]] += point[1]
        z_sum[point[3]] += point[2]
    
    centers = []
    for cluster in range(n):
        center = [y_sum[cluster]/n_points[cluster], x_sum[cluster]/n_points[cluster], z_sum[cluster]/n_points[cluster]]
        centers.append(center)
    centers = np.asarray(centers)

    data2d = data2d[:, 0:3]
    
    return centers, labels
In [89]:
def sort_colors(centers, colorspace, method):

    if colorspace == "RGB":
        centers_rgb = list(centers)
        
    elif colorspace == "HSV":
        #centers to rgb
        centers_rgb = []
        for color in centers:
            hsv_string = f"hsv({color[0]},{int(color[1]/255*100)}%,{int(color[2]/255*100)}%)"
            rgb = ImageColor.getrgb(hsv_string)
            centers_rgb.append(rgb)
        
    elif colorspace == "YCbCr":
        def ycbcr2rgb(color):
            Y, Cb, Cr = color
            r = int(Y + 1.40200 * (Cr - 0x80))
            g = int(Y - 0.34414 * (Cb - 0x80) - 0.71414 * (Cr - 0x80))
            b = int(Y + 1.77200 * (Cb - 0x80))

            r = max(0, min(255, r))
            g = max(0, min(255, g))
            b = max(0, min(255, b))
            
            return [r, g, b]
        
        centers_rgb = []
        for color in centers:
            rgb = ycbcr2rgb(color)
            centers_rgb.append(rgb)
    
    if method == "luminosity":
        def lum (r,g,b):
            return math.sqrt( .241 * r + .691 * g + .068 * b )
        centers_rgb.sort(key=lambda rgb: lum(*rgb)    )
        centers_sorted = centers_rgb
        
    elif method == "step_sort":
        def step(r,g,b, repetitions=1):
            lum = math.pow((0.299 * r + 0.587 * g + 0.114 * b), 1/2.2) #gamma corrected formula
            h, s, v = colorsys.rgb_to_hsv(r,g,b)
            h2 = int(h * repetitions)
            #lum2 = int(lum * repetitions)
            v2 = int(v * repetitions)
            if h2 % 2 == 1:
                v2 = repetitions - v2
                lum = repetitions - lum
            return (h2, lum, v2)
        
        centers_rgb.sort(key=lambda rgb: step(*rgb,8))
        centers_sorted = centers_rgb
        print(centers_sorted)
    
    elif method == "step_sort0":
        def step (r,g,b, repetitions=1):
            lum = math.sqrt( .241 * r + .691 * g + .068 * b )
            h, s, v = colorsys.rgb_to_hsv(r,g,b)
            h2 = int(h * repetitions)
            lum2 = int(lum * repetitions)
            v2 = int(v * repetitions)
            return (h2, lum, v2)
        centers_rgb.sort(key=lambda rgb: step(*rgb,8))
        centers_sorted = centers_rgb
        print(centers_sorted)
    
    #to manually sort, as method we can put a list with the center indices in order
    elif type(method) == list and len(method) == len(centers_rgb):
        centers_sorted = centers_rgb[method]
    
    elif method == "":
        centers_sorted = centers_rgb
        
    else:    
        print("Non-existant method")
        
    centers_sorted = np.asarray(centers_sorted)
        
    return centers_sorted
In [7]:
color = [12,59,177]
hsv_string = f"hsv({color[0]},{int(color[1]/255*100)}%,{int(color[2]/255*100)}%)"
print(hsv_string)
rgb = ImageColor.getrgb(hsv_string)
rgb
hsv(12,23%,69%)
Out[7]:
(176, 144, 135)
In [8]:
def image_from_centers(filename, data, centers_sorted):
    
    centers_sorted = np.asarray(centers_sorted).astype(int)
    n = len(centers_sorted)
    
    #data to rgb
    data_rgb = np.asarray(Image.open(filename))
        
    #create new array with personalized sizes, default color value white?
    mc_ratio = 9 #large in colorpalette cinema
    
    height = data_rgb.shape[0]
    width = data_rgb.shape[1]
    margin = width/(n*mc_ratio+n+1)
    
    palette = np.full((int(margin*(mc_ratio+2)), width, 3), 255, dtype='uint8')
    
    #calculate regions to fill with color, fill them with sorted centers
    for color in range(n):
        curr_col = centers_sorted[color]
        
        for h in range(int(margin), int(margin*(mc_ratio+1)), 1):
            for w in range(int((color+1)*margin*(mc_ratio+1) - margin*mc_ratio), int((color+1)*margin*(mc_ratio+1)), 1):
                palette[h][w] = curr_col
    
    #attach to data array
    full_arr = np.append(data_rgb, palette, axis=0)
    
    image_new = Image.fromarray(full_arr)
    display(image_new)
In [30]:
def palette_plots(filename, n, colorspace, method):
    """Master function"""
    
    data, data2d = image_to_numpy(filename, colorspace)
    print(f"Number of pixels in original image: {data2d.shape[0]}")
    data_red, data2d_red, scaling_param = reduce_image_res(filename, colorspace)
    print(f"Take every {scaling_param}-th pixel")
    print(f"Number of data points in reduced image: {data2d_red.shape[0]}")
    
    centers, labels = agglomerative(data2d_red, n)
    scatterplot_cs(data2d_red, colorspace, labels)
    
    print("Cluster centers:")
    scatterplot_cs(centers, colorspace)
    
    centers = sort_colors(centers, colorspace, method)
    
    image_from_centers(filename, data, centers)
    
    return data, centers
In [ ]:
def palette_only(filename, n, clustering, method=""):
    """Master function"""
    
    data, data2d = image_to_numpy(filename)
    data_red, data2d_red, scaling_param = reduce_image_res(filename)
    
    centers, labels = agglomerative(data2d_red, n, 'complete')

    if method != "":
        centers = sort_colors(centers, method=method)
    
    image_from_centers(data, centers)
    
    return data, centers

TEST

In [33]:
filename = "london-majus1.jpg"
data, centers = palette_plots(filename, 10, "HSV", method="dark_to_light")
Number of pixels in original image: 611520
Take every 5-th pixel
Number of data points in reduced image: 24576
Cluster centers:
In [34]:
data, centers = palette_plots(filename, 10, "RGB", method="dark_to_light")
Number of pixels in original image: 611520
Take every 5-th pixel
Number of data points in reduced image: 24576
Cluster centers:
In [96]:
data, centers = palette_plots(filename, 10, "YCbCr", method="luminosity")
Number of pixels in original image: 611520
Take every 5-th pixel
Number of data points in reduced image: 24576
Cluster centers:
In [41]:
data, centers = palette_plots('space_odyssey.jpg', 10, "HSV", method="luminosity")
Number of pixels in original image: 1047645
Take every 7-th pixel
Number of data points in reduced image: 21600
Cluster centers:
In [42]:
data, centers = palette_plots('space_odyssey.jpg', 10, "RGB", method="luminosity")
Number of pixels in original image: 1047645
Take every 7-th pixel
Number of data points in reduced image: 21600
Cluster centers:
In [85]:
data, centers = palette_plots('space_odyssey.jpg', 10, "YCbCr", method="step_sort")
Number of pixels in original image: 1047645
Take every 7-th pixel
Number of data points in reduced image: 21600
Cluster centers:
[[7, 1, 1], [62, 10, 6], [99, 48, 24], [164, 60, 40], [134, 103, 92], [157, 146, 141], [184, 205, 217], [111, 95, 191], [98, 75, 133], [64, 36, 88]]
In [98]:
data, centers = palette_plots('BladeRunner-Neon-1024x544.jpg', 10, "RGB", method="luminosity")
Number of pixels in original image: 557056
Take every 5-th pixel
Number of data points in reduced image: 22345
Cluster centers:
In [97]:
data, centers = palette_plots('BladeRunner-Neon-1024x544.jpg', 10, "YCbCr", method="luminosity")
Number of pixels in original image: 557056
Take every 5-th pixel
Number of data points in reduced image: 22345
Cluster centers: